1 using System;
2 using System.Collections.Generic;
3 using UnityEngine;
4 using UnityEngine.Assertions;
5 using Object = UnityEngine.Object;
6 using Random = UnityEngine.Random;
7
8 namespace ProceduralToolkit.Examples
9 {
10 public enum BrickSize
11 {
12 Narrow,
13 Wide,
14 }
15
16 /// <summary>
17 /// Breakout clone with procedurally generated levels
18 /// </summary>
19 public class Breakout
20 {
21 [Serializable]
22 public class Config
23 {
24 public int wallWidth = 9;
25 public int wallHeight = 7;
26 public int wallHeightOffset = 5;
27 public float paddleWidth = 1;
28 public float ballSize = 0.5f;
29 public float ballVelocityMagnitude = 5;
30 public Gradient gradient;
31 }
32
33 private const float brickColorMinValue = 0.6f;
34 private const float brickColorMaxValue = 0.8f;
35
36 private const float brickHeight = 0.5f;
37 private const float paddleHeight = 0.5f;
38 private const float paddleSpeed = 25f;
39 private const float ballRadius = 0.5f;
40 private const float ballForce = 200;
41
42 private Config config;
43
44 private Transform bricksContainer;
45 private Sprite whiteSprite;
46 private PhysicsMaterial2D bouncyMaterial;
47 private GameObject borders;
48 private GameObject paddle;
49 private Transform paddleTransform;
50 private GameObject ball;
51 private Transform ballTransform;
52 private Rigidbody2D ballRigidbody;
53 private List<GameObject> bricks = new List<GameObject>();
54
55 private Dictionary<BrickSize, float> sizeValues = new Dictionary<BrickSize, float>
56 {
57 {BrickSize.Narrow, 0.5f},
58 {BrickSize.Wide, 1f},
59 };
60
61 public Breakout()
62 {
63 bricksContainer = new GameObject("Bricks").transform;
64
65 // Generate texture and sprite for bricks, paddle and ball
66 var texture = Texture2D.whiteTexture;
67 whiteSprite = Sprite.Create(texture,
68 rect: new Rect(0, 0, texture.width, texture.width),
69 pivot: new Vector2(0.5f, 0.5f),
70 pixelsPerUnit: texture.width);
71
72 // Bouncy material for walls, paddle and everything else
73 bouncyMaterial = new PhysicsMaterial2D {name = "Bouncy", bounciness = 1, friction = 0};
74 }
75
76 public void Generate(Config config)
77 {
78 Assert.IsTrue(config.wallWidth > 0);
79 Assert.IsTrue(config.wallHeight > 0);
80 Assert.IsTrue(config.paddleWidth > 0);
81 Assert.IsTrue(config.ballSize > 0);
82
83 this.config = config;
84 ResetLevel();
85 }
86
87 public void Update()
88 {
89 float delta = Input.GetAxis("Horizontal")*Time.deltaTime*paddleSpeed;
90 paddleTransform.position += new Vector3(delta, 0);
91
92 // Prevent paddle from penetrating walls
93 float halfWall = (config.wallWidth - 1)/2f;
94 if (paddleTransform.position.x > halfWall)
95 {
96 paddleTransform.position = new Vector3(halfWall, 0);
97 }
98 if (paddleTransform.position.x < -halfWall)
99 {
100 paddleTransform.position = new Vector3(-halfWall, 0);
101 }
102
103 // Ball should move with constant velocity
104 ballRigidbody.velocity = ballRigidbody.velocity.normalized*config.ballVelocityMagnitude;
105
106 if (ballTransform.position.y < -0.1f)
107 {
108 ResetLevel();
109 }
110
111 float angle = Vector2.Angle(ballRigidbody.velocity, Vector2.right);
112 if (angle < 30 || angle > 150)
113 {
114 // Prevent ball from bouncing between walls
115 KickBallInRandomDirection();
116 }
117 }
118
119 private void ResetLevel()
120 {
121 GenerateBorders();
122 GenerateLevel();
123 GeneratePaddle();
124 GenerateBall();
125 }
126
127 private void GenerateBorders()
128 {
129 if (borders != null)
130 {
131 Object.Destroy(borders);
132 }
133 borders = new GameObject("Border");
134 float bordersHeight = config.wallHeightOffset + config.wallHeight/2 + 1 + config.ballSize;
135 float bordersWidth = config.wallWidth + 1;
136
137 // Bottom
138 CreateBoxCollider(offset: new Vector2(0, -1 - config.ballSize/2),
139 size: new Vector2(bordersWidth, 1));
140 // Left
141 CreateBoxCollider(offset: new Vector2(-bordersWidth/2f, bordersHeight/2f - 0.5f - config.ballSize/2),
142 size: new Vector2(1, bordersHeight + 1));
143 // Right
144 CreateBoxCollider(offset: new Vector2(bordersWidth/2f, bordersHeight/2f - 0.5f - config.ballSize/2),
145 size: new Vector2(1, bordersHeight + 1));
146 // Top
147 CreateBoxCollider(offset: new Vector2(0, bordersHeight - config.ballSize/2),
148 size: new Vector2(bordersWidth, 1));
149 }
150
151 private void CreateBoxCollider(Vector2 offset, Vector2 size)
152 {
153 var collider = borders.AddComponent<BoxCollider2D>();
154 collider.sharedMaterial = bouncyMaterial;
155 collider.offset = offset;
156 collider.size = size;
157 }
158
159 private void GenerateLevel()
160 {
161 // Destroy existing bricks
162 foreach (var brick in bricks)
163 {
164 Object.Destroy(brick);
165 }
166 bricks.Clear();
167
168 for (int y = 0; y < config.wallHeight; y++)
169 {
170 // Select color for current line
171 var currentColor = new ColorHSV(config.gradient.Evaluate(y/(config.wallHeight - 1f)));
172
173 // Generate brick sizes for current line
174 List<BrickSize> brickSizes = FillWallWithBricks(config.wallWidth);
175
176 Vector3 leftEdge = Vector3.left*config.wallWidth/2 +
177 Vector3.up*(config.wallHeightOffset + y*brickHeight);
178 for (int i = 0; i < brickSizes.Count; i++)
179 {
180 var brickSize = brickSizes[i];
181 var position = leftEdge + Vector3.right*sizeValues[brickSize]/2;
182
183 // Randomize tint of current brick
184 float colorValue = Random.Range(brickColorMinValue, brickColorMaxValue);
185 Color color = currentColor.WithV(colorValue).ToColor();
186
187 bricks.Add(GenerateBrick(position, color, brickSize));
188 leftEdge.x += sizeValues[brickSize];
189 }
190 }
191 }
192
193 private List<BrickSize> FillWallWithBricks(float width)
194 {
195 // https://en.wikipedia.org/wiki/Knapsack_problem
196 // We are using knapsack problem solver to fill fixed width with bricks of random width
197 Dictionary<BrickSize, int> knapsack;
198 float knapsackWidth;
199 do
200 {
201 // Prefill knapsack to get nicer distribution of widths
202 knapsack = GetRandomKnapsack(width);
203 // Calculate sum of brick widths in knapsack
204 knapsackWidth = KnapsackWidth(knapsack);
205 } while (knapsackWidth > width);
206
207 width -= knapsackWidth;
208 knapsack = PTUtils.Knapsack(sizeValues, width, knapsack);
209 var brickSizes = new List<BrickSize>();
210 foreach (var pair in knapsack)
211 {
212 for (var i = 0; i < pair.Value; i++)
213 {
214 brickSizes.Add(pair.Key);
215 }
216 }
217 brickSizes.Shuffle();
218 return brickSizes;
219 }
220
221 private Dictionary<BrickSize, int> GetRandomKnapsack(float width)
222 {
223 var knapsack = new Dictionary<BrickSize, int>();
224 foreach (var key in sizeValues.Keys)
225 {
226 knapsack[key] = (int) Random.Range(0, width/3);
227 }
228 return knapsack;
229 }
230
231 private float KnapsackWidth(Dictionary<BrickSize, int> knapsack)
232 {
233 float knapsackWidth = 0f;
234 foreach (var key in knapsack.Keys)
235 {
236 knapsackWidth += knapsack[key]*sizeValues[key];
237 }
238 return knapsackWidth;
239 }
240
241 private GameObject GenerateBrick(Vector3 position, Color color, BrickSize size)
242 {
243 var brick = new GameObject("Brick");
244 brick.transform.position = position;
245 brick.transform.parent = bricksContainer;
246 brick.transform.localScale = new Vector3(sizeValues[size], brickHeight);
247
248 var brickRenderer = brick.AddComponent<SpriteRenderer>();
249 brickRenderer.sprite = whiteSprite;
250 brickRenderer.color = color;
251
252 var brickCollider = brick.AddComponent<BoxCollider2D>();
253 brickCollider.sharedMaterial = bouncyMaterial;
254 brick.AddComponent<Brick>();
255 return brick;
256 }
257
258 private void GeneratePaddle()
259 {
260 if (paddle == null)
261 {
262 paddle = new GameObject("Paddle");
263 paddleTransform = paddle.transform;
264
265 var paddleRenderer = paddle.AddComponent<SpriteRenderer>();
266 paddleRenderer.sprite = whiteSprite;
267 paddleRenderer.color = Color.black;
268
269 var paddleCollider = paddle.AddComponent<BoxCollider2D>();
270 paddleCollider.sharedMaterial = bouncyMaterial;
271 }
272
273 paddleTransform.position = Vector3.zero;
274 paddleTransform.localScale = new Vector3(config.paddleWidth, paddleHeight);
275 }
276
277 private void GenerateBall()
278 {
279 if (ball == null)
280 {
281 ball = new GameObject("Ball");
282 ballTransform = ball.transform;
283
284 var ballRenderer = ball.AddComponent<SpriteRenderer>();
285 ballRenderer.sprite = whiteSprite;
286 ballRenderer.color = Color.black;
287
288 var ballCollider = ball.AddComponent<CircleCollider2D>();
289 ballCollider.radius = ballRadius;
290 ballCollider.sharedMaterial = bouncyMaterial;
291
292 ballRigidbody = ball.AddComponent<Rigidbody2D>();
293 ballRigidbody.gravityScale = 0;
294 ballRigidbody.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
295 ballRigidbody.constraints = RigidbodyConstraints2D.FreezeRotation;
296 }
297
298 ballTransform.position = Vector3.up;
299 ballTransform.localScale = new Vector3(config.ballSize, config.ballSize);
300 ballRigidbody.velocity = Vector2.zero;
301 KickBallInRandomDirection();
302 }
303
304 private void KickBallInRandomDirection()
305 {
306 Vector2 direction = Random.Range(-0.5f, 0.5f)*Vector2.right + Vector2.up;
307 ballRigidbody.AddForce(direction*ballForce);
308 }
309 }
310 }